import { AbstractRef, ArkClass, ArkFile, ArkMethod, BasicBlock } from '../src';
import * as Expr from '../src/core/base/Expr';
import * as Stmt from '../src/core/base/Stmt';

export class Node {
    private readonly nodeName: string;
    private readonly label: string;
    private readonly children: Node[];

    constructor(nodeName: string, label: string) {
        this.nodeName = nodeName;
        this.label = label;
        this.children = [];
    }

    public addChild(node: Node) {
        this.children.push(node);
    }

    public getChildren() {
        return this.children;
    }

    public getNodeName() {
        return this.nodeName;
    }

    public getLabel() {
        return this.label;
    }
}

export class FileNode extends Node {
    constructor(arkFile: ArkFile) {
        super(arkFile.getName(), 'FileNode');
    }
}

export class ClassNode extends Node {
    constructor(arkClass: ArkClass) {
        super(arkClass.getName(), 'ClassNode');
    }
}

export class BlockNode extends Node {
    constructor(block: BasicBlock) {
        super(block.toString(), 'BlockNode');
    }

    public addChild(node: StmtNode) {
        super.addChild(node);
        node.addChild();
    }
}

export class MethodNode extends Node {
    constructor(arkMethod: ArkMethod) {
        super(arkMethod.getName(), 'MethodNode');
    }
}

export class StmtNode extends Node {
    private readonly stmt: Stmt.Stmt;

    constructor(arkStmt: Stmt.Stmt) {
        super(arkStmt.toString(), arkStmt.constructor.toString());
        this.stmt = arkStmt;
    }

    public getStmt() {
        return this.stmt;
    }

    public addChild() {
        switch (this.stmt.constructor.name) {
            case 'ArkAssignStmt':
                const leftOp = (this.stmt as Stmt.ArkAssignStmt).getLeftOp();
                const rightOp = (this.stmt as Stmt.ArkAssignStmt).getRightOp();
                const regex = /Ref/;
                if (leftOp.constructor.name === 'Local') {
                    this.getChildren().push(new Node(leftOp.toString(), 'LeftNode'));
                }
                if (rightOp.constructor.name === 'Local') {
                    this.getChildren().push(new Node(rightOp.toString(), 'RightNode'));
                } else if (regex.test(rightOp.constructor.name)) {
                    this.getChildren().push(new Node(rightOp.toString(), rightOp.constructor.name));
                } else {
                    const anotherRegex = /Expr/;
                    const x = anotherRegex.test(rightOp.constructor.name);
                    if (x) {
                        const normalBinaryExpr = rightOp as Expr.ArkNormalBinopExpr;
                        const normalBinaryExprNode = new ExprNode(normalBinaryExpr);
                        this.getChildren().push(normalBinaryExprNode);
                        normalBinaryExprNode.addChild();
                    } else {
                        this.getChildren().push(new Node(rightOp.toString(), 'ConstantNode'));
                    }
                }
                break;
            case 'ArkReturnStmt':
                const arkReturnStmt = this.stmt as Stmt.ArkReturnStmt;
                this.getChildren().push(new Node('return', 'ReturnNode'));
                if (arkReturnStmt.getOp()) {
                    this.getChildren().push(new Node(arkReturnStmt.getOp().toString(), 'ReturnValue'));
                }
                break;
            case 'ArkIfStmt':
                const ifStmt = this.stmt as Stmt.ArkIfStmt;
                this.getChildren().push(new Node('if', 'IfNode'));
                const conditionExpr = ifStmt.getConditionExprExpr();
                const conditionExprNode = new ExprNode(conditionExpr);
                conditionExprNode.addChild();
                this.getChildren().push(conditionExprNode);
                break;
            case 'ArkInvokeStmt':
                const invokeStmt = this.stmt as Stmt.ArkInvokeStmt;
                const invokeExpr = invokeStmt.getInvokeExpr();
                const invokeExprNode = new ExprNode(invokeExpr);
                this.getChildren().push(invokeExprNode);
                invokeExprNode.addChild();
                break;
        }
    }
}

export class ExprNode extends Node {
    private readonly expr: Expr.AbstractExpr;

    constructor(expr: Expr.AbstractExpr) {
        super(expr.toString(), expr.constructor.name);
        this.expr = expr;
    }

    public getExpr() {
        return this.expr;
    }

    public addChild() {
        switch (this.expr.constructor.name) {
            case 'ArkNormalBinopExpr':
                const normalBinaryExpr = this.expr as Expr.ArkNormalBinopExpr;
                const leftOp = normalBinaryExpr.getOp1();
                const rightOp = normalBinaryExpr.getOp2();
                const Operator = normalBinaryExpr.getOperator();
                this.getChildren().push(new Node(leftOp.toString(), 'Op1'));
                this.getChildren().push(new Node(Operator.toString(), 'Operator'));
                this.getChildren().push(new Node(rightOp.toString(), 'Op2'));
                break;
            case 'ArkStaticInvokeExpr':
                const staticInvokeExpr = this.expr as Expr.ArkStaticInvokeExpr;
                const arrays = staticInvokeExpr.getArgs();
                this.getChildren().push(new Node(staticInvokeExpr.toString(), 'Method'));
                for (let i = 0; i < arrays.length; i++) {
                    this.getChildren().push(new Node(arrays[i].toString(), 'Array'));
                }
                break;
            case 'ArkConditionExpr':
                const conditionExpr = this.expr as Expr.ArkConditionExpr;
                const condOp1 = conditionExpr.getOp1();
                const condOp2 = conditionExpr.getOp2();
                const condOperator = conditionExpr.getOperator();
                this.getChildren().push(new Node(condOp1.toString(), 'Op1'));
                this.getChildren().push(new Node(condOperator.toString(), 'Operator'));
                this.getChildren().push(new Node(condOp2.toString(), 'Op2'));
        }
    }
}

export class RefNode extends Node {
    constructor(ref: AbstractRef) {
        super(ref.toString(), ref.constructor.name);
    }
}

export class Edge {
    private readonly startNode: string;
    private readonly endNode: string;
    constructor(startNode: string, endNode: string) {
        this.startNode = startNode;
        this.endNode = endNode;
    }
    public getStartNode() {
        return this.startNode;
    }
    public getEndNode() {
        return this.endNode;
    }
}
